# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: xguo

import coin.exchange.base.kr_rest.level_book as level_book_mdl
import coin.exchange.base.kr_rest.public_client_base as pubcb
import coin.exchange.bitmex.kr_rest.native_public_client as npubc
from coin.exchange.bitmex.kr_rest.futures_product import BitmexFuturesProduct

from coin.proto.coin_market_query_pb2 import (
    ProductTicker,
    ExchangeTicker,
    ProductKlineElement,
    ProductKline,
)


def _float_if_exists(elem, key):
  if key in elem and elem[key] is not None:
    return float(elem[key])
  else:
    return None


class BitmexFeedParser(object):
  @staticmethod
  def parse_native_ticker(update_msg, product):
    assert len(update_msg) == 1
    elem = update_msg[0]
    assert elem['symbol'] == product.native_symbol
    product_ticker = ProductTicker(high=_float_if_exists(elem, 'highPrice'),
                                   low=_float_if_exists(elem, 'lowPrice'),
                                   last=_float_if_exists(elem, 'lastPrice'),
                                   bid=_float_if_exists(elem, 'bidPrice'),
                                   ask=_float_if_exists(elem, 'askPrice'),
                                   exchange_timestamp=int(elem['timestamp'].timestamp() * 1e+9),
                                   volume_24hr=_float_if_exists(elem, 'volume24h'),
                                   symbol=product.symbol,
                                   native_symbol=product.native_symbol)
    return product_ticker

  @staticmethod
  def parse_exchange_ticker(update_msg):
    product_ticker_list = []
    for elem in update_msg:
      try:
        product = BitmexFuturesProduct.FromStr(elem['symbol'])
      except Exception:
        continue
      product_ticker = ProductTicker(high=_float_if_exists(elem, 'highPrice'),
                                     low=_float_if_exists(elem, 'lowPrice'),
                                     last=_float_if_exists(elem, 'lastPrice'),
                                     bid=_float_if_exists(elem, 'bidPrice'),
                                     ask=_float_if_exists(elem, 'askPrice'),
                                     exchange_timestamp=int(elem['timestamp'].timestamp() * 1e+9),
                                     volume_24hr=_float_if_exists(elem, 'volume24h'),
                                     symbol=product.symbol,
                                     native_symbol=product.native_symbol)
      product_ticker_list.append(product_ticker)
    assert product_ticker_list
    return ExchangeTicker(exchange="Bitmex", each_ticker=product_ticker_list)

  @staticmethod
  def parse_native_kline(update_msg, product, kline_period):
    """Sample kline response:
      [
        {
          "timestamp": "2019-04-08T01:33:04.459Z",
          "symbol": "string",
          "open": 0,
          "high": 0,
          "low": 0,
          "close": 0,
          "trades": 0,
          "volume": 0,
          "vwap": 0,
          "lastSize": 0,
          "turnover": 0,
          "homeNotional": 0,
          "foreignNotional": 0
        }
      ]
    """
    klines = update_msg
    kline_group = []
    for kline in klines:
      product_kline = ProductKlineElement(
          kline_timestamp=int(kline['timestamp'].timestamp() * 1e+9),
          open=float(kline['open']),
          high=float(kline['high']),
          low=float(kline['low']),
          close=float(kline['close']),
          volume=float(kline['volume']),
      )
      kline_group.append(product_kline)
    return ProductKline(
        symbol=product.symbol,
        native_symbol=product.native_symbol,
        exchange='Bitmex',
        market_type='Futures',
        kline_period=kline_period,
        klines=kline_group,
    )

  @staticmethod
  def parse_level_book(update_msg, product):
    # Orderbook update message is a list of orders sorted according to their
    # price in descending order.
    assert all(update_msg[n]['price'] > update_msg[n + 1]['price']
               for n in range(len(update_msg) - 1)), 'orderbook not properly sorted'

    level_book = level_book_mdl.LevelBook(product)
    for elem in update_msg:
      level_elem = level_book_mdl.LevelElement(float(elem['price']), float(elem['size']), 0)
      if elem['side'] == 'Sell':
        level_book.add_ask(level_elem)
      elif elem['side'] == 'Buy':
        level_book.add_bid(level_elem)
      else:
        raise ValueError('Unknown order side in the orderbook')
    level_book.finalize()
    return level_book


class BitmexPublicClient(pubcb.PublicClientBase):
  def __init__(self, timeout=5, test=False):
    self.npubc = npubc.BitmexNativePublicClient(timeout=timeout, test=test)

  def query_ticker_impl(self, product):
    symbol = product.native_symbol
    update = self.npubc.get_instrument_open(symbol)
    update.msg = BitmexFeedParser.parse_native_ticker(update.msg, product)
    return update

  def query_exchange_ticker_impl(self):
    update = self.npubc.get_instrument_open()
    update.msg = BitmexFeedParser.parse_exchange_ticker(update.msg)
    return update

  def query_history_kline_impl(self, product, kline_period, start_time, end_time):
    symbol = product.native_symbol
    update = self.npubc.query_trade_bucketed(symbol, kline_period, start_time, end_time)
    update.msg = BitmexFeedParser.parse_native_kline(update.msg, product, kline_period)
    return update

  def query_level_book_impl(self, product):
    symbol = product.native_symbol
    update = self.npubc.get_orderbook_l2(symbol)
    update.msg = BitmexFeedParser.parse_level_book(update.msg, product)
    return update
